/*
    TreeViewer - Cross-platform software to draw phylogenetic trees
    Copyright (C) 2023  Giorgio Bianchini, University of Bristol
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

using PhyloTree;
using System.Collections.Generic;
using VectSharp;
using TreeViewer;
using System;
using System.Linq;
using System.Runtime.InteropServices;

namespace ab93f8a2b8731465892f5bb80af7292a8
{
    /// <summary>
    /// This module plots age distributions in a timeline below (or above, or both) the tree. This is useful to highlight the age distribution
    /// for particular nodes, without showing it for all nodes.
    /// 
    /// The nodes for which a distribution is shown can be chosen by using the [Colour](#colour) parameter: if this returns a transparent colour,
    /// the distribution is not shown; otherwise it is drawn with the specified colour.
    /// </summary>
    /// 
    /// <description>
    /// <![CDATA[
    /// ## Further information
    /// 
    /// By combining this module with the _Scale axis_ module, it is possible to obtain timeline plots such as the following:
    /// 
    /// <p align="center">
    ///     <img height="600" src="">
    /// </p>
    ///
    /// Note how there is no tree in the plot. This is because the _Branches_ module was not used.
    /// 
    /// ]]>
    /// </description>
    public static class MyModule
    {
        public const string Name = "Age distributions timeline";
        public const string HelpText = "Plots age distributions on a timeline.";
        public const string Author = "Giorgio Bianchini";
        public static Version Version = new Version("1.1.2");
        public const ModuleTypes ModuleType = ModuleTypes.Plotting;

        public const string Id = "b93f8a2b-8731-4658-92f5-bb80af7292a8";

        private static string Icon16Base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACKSURBVDhPpZIxDoAgDEWLJ5PVxbuwGyTu3MXF2ZshYJsgEarwlv4O/eGXCkiYln31Rd8dmGObQ19lwErQcECjYZXcIIc1iRGyp5d4jST8sEPdBBeBpT8C1kghTvU7uQjsLeQGBmugOKyUGlG24Q0cmTx28JVggFKKpGmi+wU9hySttSfqf9ACAQAuuk8yz5D7TPgAAAAASUVORK5CYII=";
        private static string Icon24Base64 = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADaSURBVEhLY2TAAbxqNzYAqXogbtzW7A9ikwWYoTQKQDIcBBxU7SMZbx9acQDKJwkwQWl0ADMcBuqhlpIMMCzAYxBZlqAEEVrQYAMkBxc4kokwGBfYCsRn8CUCRqDh/6FsmgBckUw1QHMLaB8HUBoMiLSIpJyNkkxByQ+UDIFMB4gIBiC52MBaFuFIWWSVSbgiuRFKwwDZBR7Wwg4tqCgqTYceKCoq+g/E8ERCq4y2H2YJ1lRECQD5AMoEAUdGNAGqA/qURdQE6EFESx849vX1HaCVBWDDGRgYGACW61Uxk1qd/wAAAABJRU5ErkJggg==";
        private static string Icon32Base64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEWSURBVFhH7ZYxEoIwEEUTPYvnEFoba69B7yhjzzlsLbSVsfYGHgazYcMECUmAlbXwzWR2ofB/Nm42UgTY7C9HFQ5q5bfTFnJSFhh9gLiOaIYUrwGHILmJUAXM19uQmug1EBAhM7HE2AJ/3PX1NslqvZOvx7nE51F0DESKGyabaNpwoHAfV7WeQ9pVGyAS99FrTCrxCnMWYg6ir8K/BRh5/4Q2I4xMGlKdcwB6GnpbpUn9xsvkCek8CSNNkIznzhbYeFqU7G4QasMcow3pxcRbAeCjCqTiQMxBZKpALv5ndrIsq9RqtTbHMLrbJrimYWMi2IaUwBZgakil4+Ws/MaFZC5cW8BZgbQoipLLgBaHhMNAIy6EEG8Mi3WrsRRbmAAAAABJRU5ErkJggg==";

        public static Page GetIcon(double scaling)
        {
            byte[] bytes;

            if (scaling <= 1)
            {

                bytes = Convert.FromBase64String(Icon16Base64);
            }
            else if (scaling <= 1.5)
            {
                bytes = Convert.FromBase64String(Icon24Base64);
            }
            else
            {
                bytes = Convert.FromBase64String(Icon32Base64);
            }

            IntPtr imagePtr = Marshal.AllocHGlobal(bytes.Length);
            Marshal.Copy(bytes, 0, imagePtr, bytes.Length);

            RasterImage icon;

            try
            {
                icon = new VectSharp.MuPDFUtils.RasterImageStream(imagePtr, bytes.Length, MuPDFCore.InputFileTypes.PNG);
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            finally
            {
                Marshal.FreeHGlobal(imagePtr);
            }

            Page pag = new Page(16, 16);
            pag.Graphics.DrawRasterImage(0, 0, 16, 16, icon);

            return pag;
        }

        public static List<(string, string)> GetParameters(TreeNode tree)
        {
            return new List<(string, string)>()
            {
                ( "InstanceStateData", "InstanceStateData:" ),

                ( "Age distribution", "Group:2"),

                /// <param name="Age distribution:">
                /// This parameter determines whether the age distribution that is shown is the default one (i.e., the last one that has been
                /// set up), or whether a name should be entered to specify another age distribution.
                /// </param>
                ( "Age distribution:", "ComboBox:0[\"Default\",\"Custom\"]"),

                /// <param name="Distribution name:">
                /// If the value for the [Age distribution](#age-distribution) parameter is `Custom`, this text box can be used to enter the name
                /// of the age distribution to draw (as defined within the parameters of the _Set up age distributions_ module).
                /// </param>
                ( "Distribution name:", "TextBox:" ),
                
                /// <param name="Plot type:">
                /// This parameter determines the kind of plot used to draw the age distributions. If the value is `Histogram`, histograms
                /// are drawn, displaying the age distributions. The bars of the histogram are centered so that the plot looks
                /// similar to a violin plot. The width of the bars of the histogram is chosen automatically.
                /// 
                /// If the value is `Envelope`, a violin plot is drawn, using the same samples that would be used to draw the histogram;
                /// instead of drawing rectangular bars, a smooth spline is interpolated between the sample points to produce a smooth-looking
                /// plot. Please note that this is not a kernel density estimation of the age distribution (as that would be too expensive to
                /// draw in real time).
                /// </param>
                ( "Plot type:", "ComboBox:0[\"Histogram\",\"Envelope\"]" ),

                ( "Plot layout", "Group:4" ),
                
                /// <param name="Height:">
                /// This parameter determines the height of each age distribution plot.
                /// </param>
                ( "Height:", "NumericUpDown:10[\"0\",\"Infinity\"]"),
                
                /// <param name="Spacing:">
                /// This parameter determines the spacing between consecutive distributions.
                /// </param>
                ( "Spacing:", "NumericUpDown:5[\"-Infinity\",\"Infinity\"]"),
                
                /// <param name="Margin:">
                /// This parameter determines the margin between the tree and the timeline plot.
                /// </param>
                ( "Margin:", "NumericUpDown:20[\"-Infinity\",\"Infinity\"]"),

                /// <param name="Plot position:">
                /// This parameter determines the position of the timeline plot relative to the tree plot.
                /// </param>
                ( "Plot position:", "ComboBox:1[\"Top\",\"Bottom\",\"Both\"]" ),

                ("Fill colour", "Group: 3"),
                
                /// <param name="Auto colour by node" display="Auto colour by node (distributions)">
                /// If this check box is checked, the colour of each age distribution is determined algorithmically in a pseudo-random
                /// way designed to achieve an aestethically pleasing distribution of colours, while being reproducible
                /// if the same tree is rendered multiple times.
                /// </param>
                ("Auto colour by node", "CheckBox:true"),
                
                /// <param name="Opacity:" display="Opacity (distributions)">
                /// This parameter determines the opacity of the colour used if the [Auto colour by node (distributions)](#auto-colour-by-node-distributions)
                /// option is enabled.
                /// </param>
                ("Opacity:", "Slider:0.5[\"0\",\"1\",\"{0:P0}\"]"),
                
                /// <param name="Colour:">
                /// This parameter determines the colour used to draw each age distribution (if the [Auto colour by node (distributions)](#auto-colour-by-node-distributions)
                /// option is disabled). The colour can be determined based on the value of an attribute of the nodes in the tree.
                /// For nodes that do not possess the specified attribute (or that have the attribute with an invalid value), a
                /// default value is used. The default attribute used to determine the colour is `Color`.
                /// </param>
                ("Colour:", "ColourByNode:[" + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToColour[0]) + ",\"Color\",\"String\",\"0\",\"0\",\"0\",\"255\",\"true\"]"),

                ( "Stroke style", "Group:6" ),
                
                /// <param name="Auto stroke colour by node">
                /// If this check box is checked, the colour of each age distribution is determined algorithmically in a pseudo-random
                /// way designed to achieve an aestethically pleasing distribution of colours, while being reproducible
                /// if the same tree is rendered multiple times.
                /// </param>
                ( "Auto stroke colour by node", "CheckBox:true"),
                
                /// <param name="Line opacity:">
                /// This parameter determines the opacity of the colour used if the [Auto stroke colour by node](#auto-stroke-colour-by-node)
                /// option is enabled.
                /// </param>
                ( "Line opacity:", "Slider:0.5[\"0\",\"1\",\"{0:P0}\"]" ),
                
                /// <param name="Line colour:">
                /// This parameter determines the colour used to stroke each age distribution (if the [Auto stroke colour by node](#auto-stroke-colour-by-node)
                /// option is disabled). The colour can be determined based on the value of an attribute of the nodes in the tree.
                /// For nodes that do not possess the specified attribute (or that have the attribute with an invalid value), a
                /// default value is used. The default attribute used to determine the colour is `Color`.
                /// </param>
                ( "Line colour:", "ColourByNode:[" + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToColour[0]) + ",\"Color\",\"String\",\"0\",\"0\",\"0\",\"255\",\"true\"]" ),
                
                 /// <param name="Line weight:">
                /// The thickness of the outline for the branch distributions. This can be determined based on the value of an attribute of the nodes in the tree. For nodes that
                /// do not possess the specified attribute (or that have the attribute with an invalid value), a default value is used. The default attribute
                /// used to determine the thickness of the branches is `Thickness`.
                /// </param>
                ( "Line weight:", "NumericUpDownByNode:0[\"0\",\"Infinity\"," + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToDouble[1]) + ",\"Thickness\",\"Number\",\"true\"]" ),
                
                /// <param name="Line cap:">
                /// The line cap to use when drawing the distributions.
                /// </param>
                ( "Line cap:", "ComboBox:1[\"Butt\",\"Round\",\"Square\"]" ),
                
                /// <param name="Line dash:">
                /// The line dash to use when drawing the distributions.
                /// </param>
                ( "Line dash:", "Dash:[0,0,0]"),

                ("Labels", "Group: 7"),

                /// <param name="Label position:">
                /// This parameter determines the position of each label, relative to the age distribution to which it is associated.
                /// </param>
                ("Label position:", "ComboBox:3[\"None\",\"Left\",\"Center\",\"Right\"]"),
                
                /// <param name="Margin: ">
                /// This parameter determines margin of the label with respect to the anchor point.
                /// </param>
                ("Margin: ", "Point:[10,0]"),
                
                /// <param name="Font:">
                /// This parameter determines the font used to draw the labels.
                /// </param>
                ("Font:", "Font:[\"Helvetica-Bold\",\"15\"]"),

                ( "Attribute", "Group:3" ),

                /// <param name="Attribute:">
                /// This parameter specifies the attribute used to determine the text of the labels. By default the `Name` of each node is drawn.
                /// </param>
                ( "Attribute:", "AttributeSelector:Name" ),

                /// <param name="Attribute type:">
                /// This parameter specifies the type of the attribute used to determine the text of the labels. By default this is `String`. If the type chosen here does not correspond to the actual type of the attribute
                /// (e.g. `Number` is chosen for the `Name` attribute, or `String` is chosen for the `Length` attribute), no label is drawn. If the attribute has values with different types for different nodes, the label is only shown on nodes
                /// whose attribute type corresponds to the one chosen here.
                /// </param>
                ( "Attribute type:", "AttributeType:String"),

                /// <param name="Attribute format...">
                /// This parameter determines how the value of the selected attribute is used to determine the text of the label. By default, if the [Attribute type](#attribute-type) is `String` the text of the label corresponds to the value of the attribute,
                /// while if the [Attribute type](#attribute-type) is `Number` the text of the label corresponds to the number rounded to 2 significant digits.
                /// </param>
                ( "Attribute format...", "Formatter:[\"Attribute type:\"," + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConverters[0]) + ",\"true\"]"),
                
                /// <param name="Auto colour by node " display="Auto colour by node (labels)">
                /// If this check box is checked, the colour of the label for each distribution is determined algorithmically in a pseudo-random
                /// way designed to achieve an aestethically pleasing distribution of colours, while being reproducible
                /// if the same tree is rendered multiple times.
                /// </param>
                ("Auto colour by node ", "CheckBox:true"),
                
                /// <param name="Opacity: " display="Opacity (labels)">
                /// This parameter determines the opacity of the colour used if the [Auto colour by node (labels)](#auto-colour-by-node-labels)
                /// option is enabled.
                /// </param>
                ("Opacity: ", "Slider:0.5[\"0\",\"1\",\"{0:P0}\"]"),
                
                /// <param name="Colour: ">
                /// This parameter determines the colour used to draw each label (if the [Auto colour by node (labels)](#auto-colour-by-node-labels)
                /// option is disabled). The colour can be determined based on the value of an attribute of the nodes in the tree.
                /// For nodes that do not possess the specified attribute (or that have the attribute with an invalid value), a
                /// default value is used. The default attribute used to determine the colour is `Color`.
                /// </param>
                ("Colour: ", "ColourByNode:[" + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToColour[0]) + ",\"Color\",\"String\",\"0\",\"0\",\"0\",\"255\",\"true\"]"),
            };
        }

        public static bool OnParameterChange(object tree, Dictionary<string, object> previousParameterValues, Dictionary<string, object> currentParameterValues, out Dictionary<string, ControlStatus> controlStatus, out Dictionary<string, object> parametersToChange)
        {
            controlStatus = new Dictionary<string, ControlStatus>();
            parametersToChange = new Dictionary<string, object>();

            if ((bool)currentParameterValues["Auto colour by node"])
            {
                controlStatus.Add("Opacity:", ControlStatus.Enabled);
                controlStatus.Add("Colour:", ControlStatus.Hidden);
            }
            else
            {
                controlStatus.Add("Opacity:", ControlStatus.Hidden);
                controlStatus.Add("Colour:", ControlStatus.Enabled);
            }

            if ((bool)currentParameterValues["Auto colour by node "])
            {
                controlStatus.Add("Opacity: ", ControlStatus.Enabled);
                controlStatus.Add("Colour: ", ControlStatus.Hidden);
            }
            else
            {
                controlStatus.Add("Opacity: ", ControlStatus.Hidden);
                controlStatus.Add("Colour: ", ControlStatus.Enabled);
            }

            if ((bool)currentParameterValues["Auto stroke colour by node"])
            {
                controlStatus.Add("Line opacity:", ControlStatus.Enabled);
                controlStatus.Add("Line colour:", ControlStatus.Hidden);
            }
            else
            {
                controlStatus.Add("Line opacity:", ControlStatus.Hidden);
                controlStatus.Add("Line colour:", ControlStatus.Enabled);
            }

            if ((int)currentParameterValues["Age distribution:"] == 0)
            {
                controlStatus.Add("Distribution name:", ControlStatus.Hidden);
            }
            else
            {
                controlStatus.Add("Distribution name:", ControlStatus.Enabled);
            }

            if ((int)currentParameterValues["Age distribution:"] == 1 && (int)previousParameterValues["Age distribution:"] == 0 && string.IsNullOrEmpty((string)currentParameterValues["Distribution name:"]))
            {
                try
                {
                    InstanceStateData stateData = (InstanceStateData)currentParameterValues["InstanceStateData"];

                    if (stateData.Tags.TryGetValue("4e5d3934-44e6-4fe3-b11c-bd78e5b577d0", out object distribCollect) && distribCollect is Dictionary<string, (string, object)> distributionCollection && distributionCollection.Count > 0)
                    {
                        parametersToChange.Add("Distribution name:", distributionCollection.Where(x => checkIfFurtherTransformationModuleExists(x.Key, stateData)).Last().Value.Item1);
                    }
                }
                catch { }
            }

            if ((string)previousParameterValues["Attribute:"] != (string)currentParameterValues["Attribute:"])
            {
                string attributeName = (string)currentParameterValues["Attribute:"];

                string attrType = ((TreeNode)tree).GetAttributeType(attributeName);

                if (!string.IsNullOrEmpty(attrType) && (string)previousParameterValues["Attribute type:"] == (string)currentParameterValues["Attribute type:"] && (string)currentParameterValues["Attribute type:"] != attrType)
                {
                    parametersToChange.Add("Attribute type:", attrType);

                    if (previousParameterValues["Attribute format..."] == currentParameterValues["Attribute format..."])
                    {
                        if (attrType == "String")
                        {
                            parametersToChange.Add("Attribute format...", new FormatterOptions(Modules.DefaultAttributeConverters[0]) { Parameters = new object[] { Modules.DefaultAttributeConverters[0], true } });
                        }
                        else if (attrType == "Number")
                        {
                            parametersToChange.Add("Attribute format...", new FormatterOptions(Modules.DefaultAttributeConverters[1]) { Parameters = new object[] { 0, 2.0, 0.0, 0.0, false, true, Modules.DefaultAttributeConverters[1], true } });
                        }
                    }
                }
            }

            if ((string)previousParameterValues["Attribute type:"] != (string)currentParameterValues["Attribute type:"])
            {
                string attrType = (string)currentParameterValues["Attribute type:"];

                if (previousParameterValues["Attribute format..."] == currentParameterValues["Attribute format..."])
                {
                    if (attrType == "String")
                    {
                        parametersToChange.Add("Attribute format...", new FormatterOptions(Modules.DefaultAttributeConverters[0]) { Parameters = new object[] { Modules.DefaultAttributeConverters[0], true } });
                    }
                    else if (attrType == "Number")
                    {
                        parametersToChange.Add("Attribute format...", new FormatterOptions(Modules.DefaultAttributeConverters[1]) { Parameters = new object[] { 0, 2.0, 0.0, 0.0, false, true, Modules.DefaultAttributeConverters[1], true } });
                    }
                }
            }

            return true;
        }

        public const string InvalidDistributionGuid = "527e3721-11cf-4e8c-aeec-18c93c7d8d7c";

        public static Point[] PlotAction(TreeNode tree, Dictionary<string, object> parameterValues, Dictionary<string, Point> coordinates, Graphics graphics)
        {
            bool autoColourByNode = (bool)parameterValues["Auto colour by node"];
            bool autoLabelColourByNode = (bool)parameterValues["Auto colour by node "];
            double opacity = (double)parameterValues["Opacity:"];
            double labelOpacity = (double)parameterValues["Opacity: "];

            bool defaultDistribution = (int)parameterValues["Age distribution:"] == 0;
            string distributionName = (string)parameterValues["Distribution name:"];

            InstanceStateData stateData = (InstanceStateData)parameterValues["InstanceStateData"];

            ColourFormatterOptions customColour = (ColourFormatterOptions)parameterValues["Colour:"];
            Colour defaultFill = customColour.DefaultColour;
            Func<object, Colour?> fillFormatter = customColour.Formatter;

            ColourFormatterOptions labelColour = (ColourFormatterOptions)parameterValues["Colour: "];
            Colour defaultLabelColour = labelColour.DefaultColour;
            Func<object, Colour?> labelFormatter = labelColour.Formatter;

            double maxHeight = (double)parameterValues["Height:"];
            double lineMargin = (double)parameterValues["Spacing:"];
            double margin = (double)parameterValues["Margin:"];
            int plotType = (int)parameterValues["Plot type:"];

            int labelPosition = (int)parameterValues["Label position:"];
            Point labelMargin = (Point)parameterValues["Margin: "];

            Font labelFont = (Font)parameterValues["Font:"];

            int plotPosition = (int)parameterValues["Plot position:"];

            string attribute = (string)parameterValues["Attribute:"];
            Func<object, string> formatter = ((FormatterOptions)parameterValues["Attribute format..."]).Formatter;

            bool autoStroke = (bool)parameterValues["Auto stroke colour by node"];
            double strokeOpacity = (double)parameterValues["Line opacity:"];

            ColourFormatterOptions Stroke = (ColourFormatterOptions)parameterValues["Line colour:"];
            Colour defaultStroke = Stroke.DefaultColour;
            Func<object, Colour?> strokeFormatter = Stroke.Formatter;

            NumberFormatterOptions WeightFO = (NumberFormatterOptions)parameterValues["Line weight:"];
            double defaultWeight = WeightFO.DefaultValue;
            Func<object, double?> weightFormatter = WeightFO.Formatter;

            LineCaps cap = (LineCaps)(int)parameterValues["Line cap:"];
            LineJoins join = (LineJoins)(2 - (int)parameterValues["Line cap:"]);
            LineDash dash = (LineDash)parameterValues["Line dash:"];

            Point scalePoint;

            if (!coordinates.TryGetValue("68e25ec6-5911-4741-8547-317597e1b792", out scalePoint))
            {
                throw new Exception("The coordinates module is not supported!");
            }

            if (!stateData.Tags.ContainsKey("a1ccf05a-cf3c-4ca4-83be-af56f501c2a6") || !tree.Attributes.ContainsKey("a1ccf05a-cf3c-4ca4-83be-af56f501c2a6"))
            {
                throw new Exception("The node ages have not been correctly set up!\nPlease use the \"Set up age distributions\" module.");
            }

            double minX = double.MaxValue;
            double maxX = double.MinValue;
            double minY = double.MaxValue;
            double maxY = double.MinValue;

            void updateMaxMin(Point pt)
            {
                minX = Math.Min(minX, pt.X);
                maxX = Math.Max(maxX, pt.X);
                minY = Math.Min(minY, pt.Y);
                maxY = Math.Max(maxY, pt.Y);
            }

            static Point sumPoint(Point pt1, Point pt2)
            {
                return new Point(pt1.X + pt2.X, pt1.Y + pt2.Y);
            }

            static Point rotatePoint(Point pt, double angle)
            {
                return new Point(pt.X * Math.Cos(angle) - pt.Y * Math.Sin(angle), pt.X * Math.Sin(angle) + pt.Y * Math.Cos(angle));
            }

            static Point normalizePoint(Point pt)
            {
                double modulus = Math.Sqrt(pt.X * pt.X + pt.Y * pt.Y);
                return new Point(pt.X / modulus, pt.Y / modulus);
            }

            static (int[], double[]) histogram(List<double> samples)
            {
				if (samples.Count == 1)
                {
                    return (new int[] { 1 }, new double[] { samples[0], samples[0], samples[0] * 0.01 });
                }
				
                samples.Sort();
                double iqr = BayesStats.Quantile(samples, 0.75) - BayesStats.Quantile(samples, 0.25);

                double binWidth = 2 * iqr / Math.Cbrt(samples.Count);

                double min = samples.Min();
                double max = samples.Max();
                int binCount = (int)Math.Ceiling((max - min) / binWidth);

                int[] bins = new int[binCount];

                for (int i = 0; i < samples.Count; i++)
                {
                    int bin = (int)Math.Floor((samples[i] - min) / binWidth);
                    bins[Math.Min(bin, binCount - 1)]++;
                }

                return (bins, new double[] { min, max, binWidth });
            }


            static (int[], double[], double[]) histogramWithBinMeans(List<double> samples)
            {
				if (samples.Count == 1)
                {
                    return (new int[] { 1 }, new double[] { samples[0], samples[0], samples[0] * 0.01 }, new double[] { samples[0] });
                }
				
                samples.Sort();
                double iqr = BayesStats.Quantile(samples, 0.75) - BayesStats.Quantile(samples, 0.25);

                double binWidth = 2 * iqr / Math.Cbrt(samples.Count);

                double min = samples.Min();
                double max = samples.Max();
                int binCount = (int)Math.Ceiling((max - min) / binWidth);

                int[] bins = new int[binCount];

                double[] binValues = new double[binCount];

                for (int i = 0; i < samples.Count; i++)
                {
                    int bin = (int)Math.Floor((samples[i] - min) / binWidth);
                    bins[Math.Min(bin, binCount - 1)]++;
                    binValues[Math.Min(bin, binCount - 1)] += samples[i];
                }

                for (int i = 0; i < bins.Length; i++)
                {
                    if (bins[i] > 0)
                    {
                        binValues[i] /= bins[i];
                    }
                    else
                    {
                        binValues[i] = min + binWidth * (i + 0.5);
                    }
                }

                return (bins, new double[] { min, max, binWidth }, binValues);
            }

            Point perpScale = normalizePoint(rotatePoint(scalePoint, Math.PI / 2));
            Point parallScale = normalizePoint(scalePoint);



            List<TreeNode> nodes = tree.GetChildrenRecursive();
            List<TreeNode> leaves = tree.GetLeaves();

            Point[] topAxis = null;
            Point[] bottomAxis = null;

            Point scaleNorm = normalizePoint(scalePoint);

            double maxLength = tree.LongestDownstreamLength();

            Point p0 = coordinates[tree.Id];
            Point p1 = coordinates[leaves[0].Id];
            Point p2 = coordinates[leaves[^1].Id];
            Point p3 = new Point(scalePoint.X * maxLength + p0.X, scalePoint.Y * maxLength + p0.Y);

            Point perpScalePoint = rotatePoint(scalePoint, Math.PI / 2);
            Point perpScaleNorm = normalizePoint(perpScalePoint);

            if (Math.Abs(scaleNorm.X) > 1e-4 && Math.Abs(scaleNorm.Y) > 1e-4)
            {
                double m = scalePoint.Y / scalePoint.X;
                double mp = perpScalePoint.Y / perpScalePoint.X;

                double xA = (p1.Y - m * p1.X - p0.Y + mp * p0.X) / (mp - m);
                double yA = p0.Y + mp * (xA - p0.X);

                double xB = (p2.Y - m * p2.X - p0.Y + mp * p0.X) / (mp - m);
                double yB = p0.Y + mp * (xB - p0.X);

                double xC = (p1.Y - m * p1.X - p3.Y + mp * p3.X) / (mp - m);
                double yC = p3.Y + mp * (xC - p3.X);

                double xD = (p2.Y - m * p2.X - p3.Y + mp * p3.X) / (mp - m);
                double yD = p3.Y + mp * (xD - p3.X);

                topAxis = new Point[] { new Point(xA, yA), new Point(xC, yC) };
                bottomAxis = new Point[] { new Point(xB, yB), new Point(xD, yD) };
            }
            else if (Math.Abs(scaleNorm.X) > 1e-4)
            {
                topAxis = new Point[] { new Point(p0.X, p1.Y), new Point(p3.X, p1.Y) };
                bottomAxis = new Point[] { new Point(p0.X, p2.Y), new Point(p3.X, p2.Y) };
            }
            else if (Math.Abs(scaleNorm.Y) > 1e-4)
            {
                topAxis = new Point[] { new Point(p1.X, p0.Y), new Point(p1.X, p3.Y) };
                bottomAxis = new Point[] { new Point(p2.X, p0.Y), new Point(p2.X, p3.Y) };
            }

            topAxis = new Point[] { sumPoint(topAxis[0], new Point(-perpScaleNorm.X * (margin + maxHeight), -perpScaleNorm.Y * (margin + maxHeight))), sumPoint(topAxis[1], new Point(-perpScaleNorm.X * (margin + maxHeight), -perpScaleNorm.Y * (margin + maxHeight))) };
            bottomAxis = new Point[] { sumPoint(bottomAxis[0], new Point(perpScaleNorm.X * (margin + maxHeight), perpScaleNorm.Y * (margin + maxHeight))), sumPoint(bottomAxis[1], new Point(perpScaleNorm.X * (margin + maxHeight), perpScaleNorm.Y * (margin + maxHeight))) };


            Dictionary<string, List<double>> ageDistributionsBySamples = null;
            Dictionary<string, (double[], double[])> ageDistributionFunctions = null;


            HashSet<string> availableNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            if (defaultDistribution)
            {
                if (stateData.Tags.TryGetValue("4e5d3934-44e6-4fe3-b11c-bd78e5b577d0", out object distribCollect) && distribCollect is Dictionary<string, (string, object)> distributionCollection && distributionCollection.Count > 0)
                {
                    foreach (KeyValuePair<string, (string, object)> kvp in distributionCollection)
                    {
                        if (checkIfFurtherTransformationModuleExists(kvp.Key, stateData))
                        {
                            if (kvp.Value.Item2 is Dictionary<string, List<double>> list)
                            {
                                ageDistributionsBySamples = list;
                                ageDistributionFunctions = null;
                            }
                            else if (kvp.Value.Item2 is Dictionary<string, (double[], double[])> list2)
                            {
                                ageDistributionsBySamples = null;
                                ageDistributionFunctions = list2;
                            }
                        }
                    }
                }
                else
                {
                    ageDistributionsBySamples = (Dictionary<string, List<double>>)stateData.Tags["a1ccf05a-cf3c-4ca4-83be-af56f501c2a6"];
                }
            }
            else
            {
                if (stateData.Tags.TryGetValue("4e5d3934-44e6-4fe3-b11c-bd78e5b577d0", out object distribCollect) && distribCollect is Dictionary<string, (string, object)> distributionCollection && distributionCollection.Count > 0)
                {
                    foreach (KeyValuePair<string, (string, object)> kvp in distributionCollection)
                    {
                        if (checkIfFurtherTransformationModuleExists(kvp.Key, stateData))
                        {
                            availableNames.Add(kvp.Value.Item1);

                            if (kvp.Value.Item1.Equals(distributionName, StringComparison.OrdinalIgnoreCase))
                            {
                                if (kvp.Value.Item2 is Dictionary<string, List<double>> list)
                                {
                                    ageDistributionsBySamples = list;
                                }
                                else if (kvp.Value.Item2 is Dictionary<string, (double[], double[])> list2)
                                {
                                    ageDistributionFunctions = list2;
                                }
                            }
                        }
                    }
                }
            }

            if (ageDistributionsBySamples == null && ageDistributionFunctions == null)
            {
                if (parameterValues.TryGetValue(Modules.WarningMessageControlID, out object action) && action is Action<string, string> setWarning)
                {
                    setWarning(availableNames.Aggregate("Invalid distribution specified! The available distribution names are:", (a, b) => a + "\n\n • `" + b + "`"), InvalidDistributionGuid);
                }

                return new Point[] { coordinates[Modules.RootNodeId], coordinates[Modules.RootNodeId] };
            }
            else if (parameterValues.TryGetValue(Modules.WarningMessageControlID, out object action) && action is Action<string, string> setWarning)
            {
                setWarning("", InvalidDistributionGuid);
            }

            if (plotPosition == 0 || plotPosition == 2)
            {
                RunNodePlot(nodes, ageDistributionsBySamples, ageDistributionFunctions, defaultFill, autoColourByNode, opacity, customColour, fillFormatter, plotType, histogram, histogramWithBinMeans, coordinates, scaleNorm, topAxis, sumPoint, perpScaleNorm,
                lineMargin, maxHeight, scalePoint, updateMaxMin, perpScale, graphics, labelPosition, labelColour, defaultLabelColour, autoLabelColourByNode, labelFormatter, labelMargin, labelFont, labelOpacity, -1, attribute, formatter, rotatePoint, tree.LongestDownstreamLength(),
                defaultStroke, autoStroke, Stroke, strokeFormatter, defaultWeight, WeightFO, weightFormatter, strokeOpacity, cap, join, dash);
            }

            if (plotPosition == 1 || plotPosition == 2)
            {
                RunNodePlot(nodes, ageDistributionsBySamples, ageDistributionFunctions, defaultFill, autoColourByNode, opacity, customColour, fillFormatter, plotType, histogram, histogramWithBinMeans, coordinates, scaleNorm, bottomAxis, sumPoint, perpScaleNorm,
                lineMargin, maxHeight, scalePoint, updateMaxMin, perpScale, graphics, labelPosition, labelColour, defaultLabelColour, autoLabelColourByNode, labelFormatter, labelMargin, labelFont, labelOpacity, 1, attribute, formatter, rotatePoint, tree.LongestDownstreamLength(),
                defaultStroke, autoStroke, Stroke, strokeFormatter, defaultWeight, WeightFO, weightFormatter, strokeOpacity, cap, join, dash);
            }


            return new Point[] { new Point(minX, minY), new Point(maxX, maxY) };
        }

        private static void RunNodePlot(List<TreeNode> nodes, Dictionary<string, List<double>> ageDistributionsBySamples, Dictionary<string, (double[], double[])> ageDistributionFunctions, Colour defaultFill, bool autoColourByNode, double opacity, ColourFormatterOptions customColour, Func<object, Colour?> fillFormatter,
        int plotType, Func<List<double>, (int[], double[])> histogram, Func<List<double>, (int[], double[], double[])> histogramWithBinMeans, Dictionary<string, Point> coordinates, Point scaleNorm, Point[] bottomAxis, Func<Point, Point, Point> sumPoint,
        Point perpScaleNorm, double lineMargin, double maxHeight, Point scalePoint, Action<Point> updateMaxMin, Point perpScale, Graphics graphics, int labelPosition, ColourFormatterOptions labelColour, Colour defaultLabelColour, bool autoLabelColourByNode,
        Func<object, Colour?> labelFormatter, Point labelMargin, Font labelFont, double labelOpacity, double plotDirection, string attribute, Func<object, string> formatter, Func<Point, double, Point> rotatePoint, double totalTreeLength, Colour defaultStroke,
        bool autoStroke, ColourFormatterOptions Stroke, Func<object, Colour?> strokeFormatter, double defaultWeight, NumberFormatterOptions WeightFO, Func<object, double?> weightFormatter, double strokeOpacity, LineCaps cap, LineJoins join, LineDash dash)
        {
            int shownDistribCount = 0;

            for (int i = 0; i < nodes.Count; i++)
            {
                object distributionObject = null;
                int cnt = 0;

                if (ageDistributionsBySamples != null)
                {
                    if (ageDistributionsBySamples.TryGetValue(nodes[i].Id, out List<double> ageDistribution))
                    {
                        distributionObject = ageDistribution;
                        cnt = ageDistribution.Count;
                    }
                }
                else
                {
                    if (ageDistributionFunctions.TryGetValue(nodes[i].Id, out (double[], double[]) ageDistribution))
                    {
                        distributionObject = ageDistribution;
                        cnt = 1;
                    }
                }

                if (cnt > 0)
                {
                    Colour colour = defaultFill;

                    if (autoColourByNode)
                    {
                        colour = Modules.AutoColour(nodes[i]).WithAlpha(opacity);
                    }
                    else if (nodes[i].Attributes.TryGetValue(customColour.AttributeName, out object fillAttributeObject) && fillAttributeObject != null)
                    {
                        colour = fillFormatter(fillAttributeObject) ?? defaultFill;
                    }

                    Colour strokeColour = defaultStroke;

                    if (!autoStroke)
                    {
                        if (nodes[i].Attributes.TryGetValue(Stroke.AttributeName, out object strokeAttributeObject) && strokeAttributeObject != null)
                        {
                            strokeColour = strokeFormatter(strokeAttributeObject) ?? defaultStroke;
                        }
                    }
                    else
                    {
                        strokeColour = Modules.AutoColour(nodes[i]).WithAlpha(strokeOpacity);
                    }

                    double weight = defaultWeight;

                    if (nodes[i].Attributes.TryGetValue(WeightFO.AttributeName, out object weightAttributeObject) && weightAttributeObject != null)
                    {
                        weight = weightFormatter(weightAttributeObject) ?? defaultWeight;
                    }


                    if (colour.A > 0 || (strokeColour.A > 0 && weight > 0))
                    {
                        Point leftPoint = new Point();
                        Point rightPoint = new Point();
                        bool plotted = true;

                        if (distributionObject is List<double> distrib)
                        {
                            object globalHist = null;
                            double[] range = null;

                            if (plotType == 0)
                            {
                                (int[], double[]) hist = histogram(distrib);
                                range = hist.Item2;
                                globalHist = hist;
                            }
                            else if (plotType == 1)
                            {
                                (int[], double[], double[]) hist = histogramWithBinMeans(distrib);
                                range = hist.Item2;
                                globalHist = hist;
                            }

                            double age = totalTreeLength - nodes[i].UpstreamLength();
                            double deltaLeft = age - range[1];
                            double deltaRight = range[0] - age;

                            Point coord = coordinates[nodes[i].Id];
                            double dotprod = scaleNorm.X * (coord.X - bottomAxis[0].X) + scaleNorm.Y * (coord.Y - bottomAxis[0].Y);
                            coord = sumPoint(bottomAxis[0], new Point(dotprod * scaleNorm.X, dotprod * scaleNorm.Y));
                            coord = sumPoint(coord, new Point(perpScaleNorm.X * (lineMargin + maxHeight) * shownDistribCount, perpScaleNorm.Y * (lineMargin + maxHeight * 2) * shownDistribCount * plotDirection));

                            rightPoint = sumPoint(new Point(-scalePoint.X * deltaRight, -scalePoint.Y * deltaRight), coord);
                            leftPoint = sumPoint(new Point(scalePoint.X * deltaLeft, scalePoint.Y * deltaLeft), coord);

                            updateMaxMin(sumPoint(leftPoint, new Point(-perpScaleNorm.X * maxHeight, -perpScaleNorm.Y * maxHeight)));
                            updateMaxMin(sumPoint(leftPoint, new Point(perpScaleNorm.X * maxHeight, perpScaleNorm.Y * maxHeight)));
                            updateMaxMin(sumPoint(rightPoint, new Point(-perpScaleNorm.X * maxHeight, -perpScaleNorm.Y * maxHeight)));
                            updateMaxMin(sumPoint(rightPoint, new Point(perpScaleNorm.X * maxHeight, perpScaleNorm.Y * maxHeight)));

                            if (plotType == 0)
                            {
                                (int[], double[]) hist = ((int[], double[]))globalHist;

                                int[] bins = hist.Item1;

                                double maxBin = bins.Max();

                                GraphicsPath histPth = new GraphicsPath();

                                if (leftPoint.X != rightPoint.X || leftPoint.Y != rightPoint.Y)
                                {

                                    for (int j = 0; j < bins.Length; j++)
                                    {
                                        Point vertDist = new Point(perpScale.X * bins[bins.Length - j - 1] / maxBin * maxHeight, perpScale.Y * bins[bins.Length - j - 1] / maxBin * maxHeight);
                                        Point binStart = sumPoint(leftPoint, new Point(scalePoint.X * range[2] * j, scalePoint.Y * range[2] * j));
                                        Point binEnd = sumPoint(leftPoint, new Point(scalePoint.X * Math.Min(range[2] * (j + 1), range[1] - range[0]), scalePoint.Y * Math.Min(range[2] * (j + 1), range[1] - range[0])));

                                        histPth.MoveTo(sumPoint(binStart, vertDist)).LineTo(sumPoint(binStart, new Point(-vertDist.X, -vertDist.Y))).LineTo(sumPoint(binEnd, new Point(-vertDist.X, -vertDist.Y))).LineTo(sumPoint(binEnd, vertDist)).Close();
                                    }

                                    if (colour.A > 0)
                                    {
                                        graphics.FillPath(histPth, colour, tag: nodes[i].Id);
                                    }

                                    if (strokeColour.A > 0 && weight > 0)
                                    {
                                        graphics.StrokePath(histPth, strokeColour, weight, cap, join, dash, tag: nodes[i].Id);
                                    }
                                }
                            }
                            else if (plotType == 1)
                            {
                                (int[], double[], double[]) hist = ((int[], double[], double[]))globalHist;

                                int[] bins = hist.Item1;
                                double[] binMeans = hist.Item3;

                                double maxBin = bins.Max();

                                List<Point> pointsUp = new List<Point>();

                                GraphicsPath histPth = new GraphicsPath();

                                pointsUp.Add(sumPoint(leftPoint, new Point(perpScale.X / maxBin * maxHeight, perpScale.Y / maxBin * maxHeight)));

                                for (int j = 0; j < bins.Length; j++)
                                {
                                    Point vertDist = new Point(perpScale.X * bins[bins.Length - j - 1] / maxBin * maxHeight, perpScale.Y * bins[bins.Length - j - 1] / maxBin * maxHeight);

                                    Point horiz = sumPoint(leftPoint, new Point(scalePoint.X * Math.Min(binMeans[j] - range[0], range[1] - range[0]), scalePoint.Y * Math.Min(binMeans[j] - range[0], range[1] - range[0])));

                                    pointsUp.Add(sumPoint(horiz, vertDist));
                                }

                                histPth.AddSmoothSpline(pointsUp.ToArray());

                                List<Point> pointsDown = new List<Point>();

                                for (int j = bins.Length - 1; j >= 0; j--)
                                {
                                    Point vertDist = new Point(-perpScale.X * bins[bins.Length - j - 1] / maxBin * maxHeight, -perpScale.Y * bins[bins.Length - j - 1] / maxBin * maxHeight);
                                    Point horiz = sumPoint(leftPoint, new Point(scalePoint.X * Math.Min(binMeans[j] - range[0], range[1] - range[0]), scalePoint.Y * Math.Min(binMeans[j] - range[0], range[1] - range[0])));

                                    pointsDown.Add(sumPoint(horiz, vertDist));
                                }

                                pointsDown.Add(sumPoint(leftPoint, new Point(perpScale.X / maxBin * maxHeight, perpScale.Y / maxBin * maxHeight)));

                                histPth.AddSmoothSpline(pointsDown.ToArray());


                                histPth.Close();

                                if (colour.A > 0)
                                {
                                    graphics.FillPath(histPth, colour, tag: nodes[i].Id);
                                }

                                if (strokeColour.A > 0 && weight > 0)
                                {
                                    graphics.StrokePath(histPth, strokeColour, weight, cap, join, dash, tag: nodes[i].Id);
                                }
                            }

                            plotted = true;
                        }
                        else if (distributionObject is (double[] sampledPoints, double[] sampledValues))
                        {
                            double[] bins = sampledValues;
                            double[] binMeans = sampledPoints;
                            double[] range = new double[] { sampledPoints.Min(), sampledPoints.Max() };

                            double maxBin = bins.Max();

                            double age = totalTreeLength - nodes[i].UpstreamLength();
                            double deltaLeft = age - range[1];
                            double deltaRight = range[0] - age;

                            if (!double.IsNaN(age) && !double.IsNaN(deltaLeft) && !double.IsNaN(deltaRight))
                            {
                                Point coord = coordinates[nodes[i].Id];
                                double dotprod = scaleNorm.X * (coord.X - bottomAxis[0].X) + scaleNorm.Y * (coord.Y - bottomAxis[0].Y);
                                coord = sumPoint(bottomAxis[0], new Point(dotprod * scaleNorm.X, dotprod * scaleNorm.Y));
                                coord = sumPoint(coord, new Point(perpScaleNorm.X * (lineMargin + maxHeight) * shownDistribCount, perpScaleNorm.Y * (lineMargin + maxHeight * 2) * shownDistribCount * plotDirection));

                                rightPoint = sumPoint(new Point(-scalePoint.X * deltaRight, -scalePoint.Y * deltaRight), coord);
                                leftPoint = sumPoint(new Point(scalePoint.X * deltaLeft, scalePoint.Y * deltaLeft), coord);

                                updateMaxMin(leftPoint);
                                updateMaxMin(rightPoint);
                                updateMaxMin(sumPoint(leftPoint, new Point(-perpScaleNorm.X * maxHeight, -perpScaleNorm.Y * maxHeight)));
                                updateMaxMin(sumPoint(leftPoint, new Point(perpScaleNorm.X * maxHeight, perpScaleNorm.Y * maxHeight)));
                                updateMaxMin(sumPoint(rightPoint, new Point(-perpScaleNorm.X * maxHeight, -perpScaleNorm.Y * maxHeight)));
                                updateMaxMin(sumPoint(rightPoint, new Point(perpScaleNorm.X * maxHeight, perpScaleNorm.Y * maxHeight)));

                                List<Point> pointsUp = new List<Point>();

                                GraphicsPath histPth = new GraphicsPath();

                                for (int j = 0; j < bins.Length; j++)
                                {
                                    Point vertDist = new Point(perpScale.X * bins[j] / maxBin * maxHeight, perpScale.Y * bins[j] / maxBin * maxHeight);

                                    Point horiz = sumPoint(rightPoint, new Point(-scalePoint.X * Math.Min(binMeans[j] - range[0], range[1] - range[0]), -scalePoint.Y * Math.Min(binMeans[j] - range[0], range[1] - range[0])));

                                    pointsUp.Add(sumPoint(horiz, vertDist));
                                }

                                for (int j = 0; j < pointsUp.Count; j++)
                                {
                                    histPth.LineTo(pointsUp[j]);
                                }

                                List<Point> pointsDown = new List<Point>();

                                for (int j = bins.Length - 1; j >= 0; j--)
                                {
                                    Point vertDist = new Point(-perpScale.X * bins[j] / maxBin * maxHeight, -perpScale.Y * bins[j] / maxBin * maxHeight);
                                    Point horiz = sumPoint(rightPoint, new Point(-scalePoint.X * Math.Min(binMeans[j] - range[0], range[1] - range[0]), -scalePoint.Y * Math.Min(binMeans[j] - range[0], range[1] - range[0])));

                                    pointsDown.Add(sumPoint(horiz, vertDist));
                                }

                                for (int j = 0; j < pointsDown.Count; j++)
                                {
                                    histPth.LineTo(pointsDown[j]);
                                }


                                histPth.Close();


                                if (colour.A > 0)
                                {
                                    graphics.FillPath(histPth, colour, tag: nodes[i].Id);
                                }

                                if (strokeColour.A > 0 && weight > 0)
                                {
                                    graphics.StrokePath(histPth, strokeColour, weight, cap, join, dash, tag: nodes[i].Id);
                                }
                            }

                            plotted = true;
                        }


                        if (labelPosition > 0 && plotted)
                        {
                            string labelText = "";

                            if (nodes[i].Attributes.TryGetValue(attribute, out object attributeObject) && attributeObject != null)
                            {
                                labelText = formatter(attributeObject);
                            }

                            Colour labelCol = defaultLabelColour;

                            if (autoLabelColourByNode)
                            {
                                labelCol = Modules.AutoColour(nodes[i]).WithAlpha(labelOpacity);
                            }
                            else if (nodes[i].Attributes.TryGetValue(labelColour.AttributeName, out object fillAttributeObject) && fillAttributeObject != null)
                            {
                                labelCol = labelFormatter(fillAttributeObject) ?? defaultLabelColour;
                            }

                            if (!string.IsNullOrEmpty(labelText) && labelCol.A > 0)
                            {
                                Size size = labelFont.MeasureText(labelText);

                                graphics.Save();

                                if (labelPosition == 1)
                                {
                                    graphics.Translate(leftPoint);
                                    graphics.Rotate(Math.Atan2(scaleNorm.Y, scaleNorm.X));

                                    graphics.FillText(sumPoint(labelMargin, new Point(-size.Width, 0)), labelText, labelFont, labelCol, TextBaselines.Middle, tag: nodes[i].Id);

                                    updateMaxMin(sumPoint(leftPoint, rotatePoint(sumPoint(labelMargin, new Point(-size.Width, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(leftPoint, rotatePoint(sumPoint(labelMargin, new Point(-size.Width, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(leftPoint, rotatePoint(sumPoint(labelMargin, new Point(0, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(leftPoint, rotatePoint(sumPoint(labelMargin, new Point(0, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                }
                                else if (labelPosition == 2)
                                {
                                    graphics.Translate(new Point((leftPoint.X + rightPoint.X) * 0.5, (leftPoint.Y + rightPoint.Y) * 0.5));
                                    graphics.Rotate(Math.Atan2(scaleNorm.Y, scaleNorm.X));

                                    graphics.FillText(sumPoint(labelMargin, new Point(-size.Width * 0.5, 0)), labelText, labelFont, labelCol, TextBaselines.Middle, tag: nodes[i].Id);

                                    updateMaxMin(sumPoint(new Point((leftPoint.X + rightPoint.X) * 0.5, (leftPoint.Y + rightPoint.Y) * 0.5), rotatePoint(sumPoint(labelMargin, new Point(-size.Width * 0.5, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(new Point((leftPoint.X + rightPoint.X) * 0.5, (leftPoint.Y + rightPoint.Y) * 0.5), rotatePoint(sumPoint(labelMargin, new Point(-size.Width * 0.5, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(new Point((leftPoint.X + rightPoint.X) * 0.5, (leftPoint.Y + rightPoint.Y) * 0.5), rotatePoint(sumPoint(labelMargin, new Point(size.Width * 0.5, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(new Point((leftPoint.X + rightPoint.X) * 0.5, (leftPoint.Y + rightPoint.Y) * 0.5), rotatePoint(sumPoint(labelMargin, new Point(size.Width * 0.5, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                }
                                else if (labelPosition == 3)
                                {
                                    graphics.Translate(rightPoint);
                                    graphics.Rotate(Math.Atan2(scaleNorm.Y, scaleNorm.X));

                                    graphics.FillText(labelMargin, labelText, labelFont, labelCol, TextBaselines.Middle, tag: nodes[i].Id);

                                    updateMaxMin(sumPoint(rightPoint, rotatePoint(sumPoint(labelMargin, new Point(size.Width, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(rightPoint, rotatePoint(sumPoint(labelMargin, new Point(size.Width, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(rightPoint, rotatePoint(sumPoint(labelMargin, new Point(0, -size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                    updateMaxMin(sumPoint(rightPoint, rotatePoint(sumPoint(labelMargin, new Point(0, size.Height * 0.5)), Math.Atan2(scaleNorm.Y, scaleNorm.X))));
                                }

                                graphics.Restore();
                            }
                        }

                        if (plotted)
                        {
                            shownDistribCount++;
                        }
                    }
                }
            }
        }

        private static bool checkIfFurtherTransformationModuleExists(string id, InstanceStateData stateData)
        {
            List<FurtherTransformationModule> furtherTransformationModules = stateData.FurtherTransformationModules();

            for (int i = 0; i < furtherTransformationModules.Count; i++)
            {
                if (stateData.GetFurtherTransformationModulesParamters(i).TryGetValue(Modules.ModuleIDKey, out object key) && key is string str && str.Equals(id))
                {
                    return true;
                }
            }

            return false;
        }
    }
}
